﻿using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Web;
using System.Windows.Threading;
using Newtonsoft.Json.Linq;

namespace MediaWikiEditor.MediaWikiAccess
{
   public class MediaWikiSite
   {
      #region Fields

      public delegate void PageLoadCallbackType (dynamic pageData);

      private readonly Dispatcher m_Dispatcher;

      private TextWriter m_Out;
      private string m_EditToken;

      #endregion

      #region Properties

      /// <summary>
      /// Gets or Sets the url of the wiki site to log in to.
      /// </summary>
      public string WikiSiteUrl { get; set; }

      /// <summary>
      /// Gets or Sets the user account to login with.
      /// </summary>
      public string UserName { get; set; }

      /// <summary>
      /// Gets or Sets the user's password to login with.
      /// </summary>
      public string UserPassword { get; set; }

      /// <summary>
      /// Gets the default domain for LDAP authentication. Additional information can
      /// be found at http://www.mediawiki.org/wiki/Extension:LDAP_Authentication.
      /// </summary>
      public string UserDomain { get; set; }

      /// <summary>
      /// Gets the media wiki generator.
      /// </summary>
      public JToken SiteInfo { get; private set; }

      /// <summary>
      /// Gets the list of site cookies.
      /// </summary>
      public CookieContainer Cookies { get; private set; }

      /// <summary>
      /// Gets or Sets the output writer.
      /// </summary>
      public TextWriter Out
      {
         get { return this.m_Out ?? (this.m_Out = Console.Out); }
         set { this.m_Out = value; }
      }

      #endregion

      #region Setup

      /// <summary>
      /// Initializes a new media wiki site.
      /// </summary>
      public MediaWikiSite ()
      {
         this.m_Dispatcher = Dispatcher.CurrentDispatcher;
         this.Cookies = new CookieContainer ();
      }

      /// <summary>
      /// Logs into the site.
      /// </summary>
      public bool Login (string token = null)
      {
         if (!this.WikiSiteUrl.StartsWith ("http"))
            this.WikiSiteUrl = "http://" + this.WikiSiteUrl;
         this.WikiSiteUrl = this.WikiSiteUrl.TrimEnd ('/');

         this.Out.WriteLine ("Attempting to Login...");
         string loginData = string.Format ("lgname={0}&lgpassword={1}&lgdomain={2}{3}",
                               HttpUtility.UrlEncode (UserName),
                               HttpUtility.UrlEncode (UserPassword),
                               HttpUtility.UrlEncode (UserDomain),
                               !string.IsNullOrEmpty (token) ?
                               string.Format ("&lgtoken={0}", HttpUtility.UrlEncode (token)) : "");
         JObject api = this.SendAndRequestData (Common.LoginUrl (this.WikiSiteUrl), loginData, true);
         if (api["login"]["result"].Value<string> () == "NeedToken")
         {
            this.Out.WriteLine ("Log in token needed, retrying...");
            return this.Login (api["login"]["token"].Value<string> ());
         }

         if (api["login"]["result"].Value<string> () == Common.ResultSuccess)
         {
            this.Out.WriteLine ("Gathering site information...");
            api = this.RequestData (Common.SiteInfoUrl (this.WikiSiteUrl));
            this.SiteInfo = api["query"]["general"];
            this.Out.WriteLine ("Logged in to {0} as {1}.", this.SiteInfo["sitename"], this.UserName);
            this.Out.WriteLine ("Getting edit token...");
            api = this.RequestData (Common.QueryEditInfoUrl (this.WikiSiteUrl, "Main Page"));
            var pageId = api["query"]["pageids"][0];
            this.m_EditToken = api["query"]["pages"][pageId.Value<string> ()]["edittoken"].Value<string> ();
            this.Out.WriteLine ("Token retrieved.");
            return true;
         }

         this.Out.WriteLine ("Login failed - {0}", api["login"]["result"].Value<string> ());
         return false;
      }

      #endregion

      #region Page Stuff

      /// <summary>
      /// Gets the requested page.
      /// </summary>
      /// <param name="pageName">Title of page to get.</param>
      /// <param name="loadContent">True to load the content of the page, false to not.</param>
      /// <returns>Requested page.</returns>
      public MediaWikiPage GetPage (string pageName, bool loadContent = true)
      {
         var page = new MediaWikiPage (this, pageName);

         if (loadContent)
         {
            //var load = new Action <MediaWikiPage> (this.LoadPage);
            //load.BeginInvoke (page, null, null);
            this.LoadPage (page);
         }

         return page;
      }

      /// <summary>
      /// Refreshes the given page.
      /// </summary>
      /// <param name="page">Page to refresh.</param>
      public void RefreshPage (MediaWikiPage page)
      {
         this.Out.WriteLine ("Refreshing page '{0}'", page.Title);
         var load = new Action<MediaWikiPage> (this.LoadPage);
         load.BeginInvoke (page, null, null);
      }

      /// <summary>
      /// Thread safe way to load the page.
      /// </summary>
      private void LoadPage (MediaWikiPage page)
      {
         var api = this.RequestData (Common.QueryPageDataUrl (this.WikiSiteUrl, page.Title));
         page.OnPageLoaded (api);
         //this.m_Dispatcher.Invoke (new Action(() => page.OnPageLoaded (api)));
         /*MainViewModel.SyncContex.Send (delegate
            {
               page.OnPageLoaded (api);
            }, null);*/
      }

      /// <summary>
      /// Saves all the content the given page.
      /// </summary>
      /// <param name="page">Page to save.</param>
      public void SavePage (MediaWikiPage page)
      {
         string postData = string.Format ("text={0}&basetimestamp={1}&token={2}", 
            HttpUtility.UrlEncode (page.Content),
            HttpUtility.UrlEncode (page.LastTimestamp),
            HttpUtility.UrlEncode (this.m_EditToken));
         var api = this.SendAndRequestData (Common.EditPageUrl (this.WikiSiteUrl, page.Title), postData, false);
         if (api["error"] != null)
         {
            if (this.Login ())
               this.SavePage (page);
         }

         if (api["edit"]["result"].Value<string> () == Common.ResultSuccess)
         {
            page.UpdateTimeAndUser ((api["edit"]["newtimestamp"] == null)
                                       ? page.LastTimestamp
                                       : api["edit"]["newtimestamp"].Value<string> (),
                                    this.UserName);
            this.Out.WriteLine ("'{0}' saved successfully.", page.Title);
         }
         else
            this.Out.WriteLine ("Edit for '{0}' failed!", page.Title);
      }

      /// <summary>
      /// Resaves all the pages in the Wiki.
      /// This is used to update semantic data if corrupted.
      /// </summary>
      public void ResaveAllPages ()
      {
         var api = this.RequestData (Common.AllPagesUrl (this.WikiSiteUrl));
         while (api["query-continue"] != null)
         {
            var pages = api ["query"] ["pages"];
            foreach (var page in pages)
            {
               var pageTitle = page.First["title"].Value<string> ();
               var wikiPage = new MediaWikiPage (this, pageTitle);
               var pageApi = this.RequestData (Common.QueryPageDataUrl (this.WikiSiteUrl, pageTitle));
               wikiPage.OnPageLoaded (pageApi);
               wikiPage.Save ();
               Thread.Sleep (2000);
            }

            var queryContinue = api["query-continue"].First.First.First;
            var prop = (JProperty)queryContinue;
            api = this.RequestData (Common.AllPagesUrl (this.WikiSiteUrl, 
               string.Format ("{0}={1}", prop.Name, HttpUtility.UrlEncode(prop.Value.Value<string> ()))));
         }
      }

      /// <summary>
      /// Moves the given page to the new name.
      /// </summary>
      /// <param name="oldName">Old page name to move.</param>
      /// <param name="newName">New name for moved paged.</param>
      /// <param name="reason">Reason for move.</param>
      /// <param name="loadContent">True to load the content of the newly moved page, otherwise false.</param>
      /// <returns>Newly moved page.</returns>
      public MediaWikiPage MovePage (string oldName, string newName, string reason, bool loadContent = false)
      {
         var postData = string.Format ("from={0}&to={1}&reason={2}&noredirect&token={3}",
                                  HttpUtility.UrlEncode (oldName), HttpUtility.UrlEncode (newName),
                                  HttpUtility.UrlEncode (reason), HttpUtility.UrlEncode (this.m_EditToken));
         var url = string.Format ("{0}/api.php?action=move&format=json", this.WikiSiteUrl.TrimEnd ('/'));
         this.SendAndRequestData (url, postData, false);
         this.Out.WriteLine ("Moved {0} to {1}", oldName, newName);
         return this.GetPage (newName, loadContent);
      }

      #endregion

      /*/// <summary>
      /// Gets a list of all the category names. Empty categories aren't returned,
      /// and categories without description pages are.
      /// </summary>
      public IEnumerable<string> GetAllCategories ()
      {
         string respStr = RequestData (this.WikiSiteUrl + "/api.php?action=query&list=allcategories&format=xml");
         var xDoc = new XmlDocument ();
         xDoc.LoadXml (respStr);
         var categories = xDoc.GetElementsByTagName ("c");
         foreach (XmlNode category in categories)
            yield return category.InnerText;
         var continueQuery = xDoc.GetElementById ("allcategories");
      }*/

      #region Send and Request Data

      /// <summary>
      /// Requests data from the given url.
      /// </summary>
      /// <param name="url">Url to request data from.</param>
      /// <param name="getCookies">True to save cookies, otherwise false.</param>
      /// <returns>Requested data.</returns>
      public JObject RequestData (string url, bool getCookies = false)
      {
         return this.SendAndRequestData (url, null, getCookies);
      }

      /// <summary>
      /// Sends and Requests data from the given url.
      /// </summary>
      /// <param name="url">Url to request data from.</param>
      /// <param name="postData">Data to post to url.</param>
      /// <param name="getCookies">True to save cookies, otherwise false.</param>
      /// <returns>Requested data.</returns>
      private JObject SendAndRequestData (string url, string postData, bool getCookies)
      {
         if (string.IsNullOrEmpty (url))
            throw new ArgumentNullException ("url", "Url is null");

         var webRequest = Common.CreateDefaultWebRequest (url);
         webRequest.AllowAutoRedirect = true;
         webRequest.CookieContainer = this.Cookies.Count == 0 ? new CookieContainer () : this.Cookies;
         webRequest.Headers.Add (HttpRequestHeader.AcceptEncoding, "gzip,deflate");

         if (!string.IsNullOrEmpty (postData))
            Common.PostData (postData, webRequest);

         var webResponse = GetWebResponse (webRequest);
         if (webResponse != null)
         {
            string response = string.Empty;
            var responseStream = Common.GetWebResponseStream (webResponse);
            if (responseStream != null)
            {
               using (var reader = new StreamReader (responseStream, Encoding.UTF8))
                  response = reader.ReadToEnd ();
            }
            if (getCookies)
            {
               foreach (Cookie cookie in webResponse.Cookies)
                  this.Cookies.Add (cookie);
            }
            webResponse.Close ();
            return JObject.Parse (response);
         }

         return null;
      }

      /// <summary>
      /// Gets the response of the given request.
      /// </summary>
      /// <param name="webRequest">Request to get response from.</param>
      /// <returns>Requested response.</returns>
      private HttpWebResponse GetWebResponse (HttpWebRequest webRequest)
      {
         for (int tries = 1; ; tries++)
         {
            try
            {
               return (HttpWebResponse)webRequest.GetResponse ();
            }
            catch (WebException ex)
            {
               if (tries == Common.TimesToRetry)
               {
                  this.Out.WriteLine (ex.Message);
                  return null;
               }
            }
         }
      }

      #endregion
   }
}
